Предыдущий урок можно посмотреть тут.Полный список уроков тут.
В предыдущем уроке мы с вами написали свой первый токен ERC20. Код мы писали на ходу и поместили практически весь функционал в один контракт. Для быстроты изучения это было хорошо. Но теперь пора научиться писать контракты по-взрослому. Вот некоторые рекомендации по написанию хорошего контракта:
- Не изобретать велосипед, а стараться брать уже готовые решения и изменять их под себя. Если задача популярная, значит до вас уже наступили на все грабли и нашли решение. Воспользуйтесь этим решением, не надо тратить свое время. Так токенов ERC20 уже написано довольно много. И на GitHub’е можно всегда посмотреть код. Например вот этот.
- Использовать устоявшиеся шаблоны проектирования. Такие шаблоны построены на основе опыта решения задач другими программистами. Так контракт токена ERC20 обычно имеет структуру наследования, которая не существенно отличается от проекта к проекту.
- Стараться выделять код который можно переиспользовать. Мы с вами в предыдущих уроках выделили контракт Ownable. И уже убедились что не зря. Мы его использовали в контракте-визитке и в нашем токене.
- Выделяйте стандарты в виде интерфейсов. Одна из причин такого подхода — в нашем коде невозможно понять какие из функций относятся к стандарту а какие нет.
Давайте посмотрим на часто-встречающуюся структуру наследования токена ERC20.
Названия контрактов могут отличаться от проекта к проекту, но мы будем ориентироваться на этот репозиторий. Таже структура но с названиями контрактов.
В предыдущем уроке мы говорили только о стандарте ERC20. А тут появился какой-то ERC179. Тут все просто. Как оказалось не всем нужна часть, которая отвечает за предоставление разрешений на снятие средств. Поэтому все функции не относящиеся к механизму предоставления разрешений вынесли в ERC179. Поэтому стандарт ERC20 этот тот же ERC179 только с дополнительными функциями и событиями.
Контракты StandartToken и BasicToken — это частичные реализации стандартов.
Поскольку наш токен полностью соответствует ERC20 то мы будем наследоваться от контракта StandartToken.
Помимо задач ERC20 в нашем токене есть еще функция отвечающая за эмиссию новых монет. Это тоже широко-используемый шаблон. Контракты, которые могут выпускать новые монеты, обычно, называют Mintable. А поскольку выпуcкать новые монеты может только владелец контракта, то Mintable наследуется от Ownable.
А теперь давайте вспомним, что в нашем контракте были проверки на отрицательный баланс, проверки на переполнение. В будущем нам могут могут пригодится проверки деления на ноль и так далее. Все эти проверки в случае неудачи прерывают выполнение функции. Они используются очень часто, поэтому принято их выделять в отдельную библиотеку безопасных математических операций — SafeMath. В большинстве проектов она скопирована один в один.
С учетом всего вышесказанного структура контрактов нашего токена теперь выглядит так
Важно понимать что от библиотек не наследуются. Их «используют», т.е. подключют к контракту. Например для SafeMath это выглядит так:
1 |
using SafeMath for uint256; |
Возьмем готовый код стандартных шаблонов из репозитория ZeppelinSolidity и перепишем наш токен:
- ERC20Basic — интерфейс ERC179 , т.е. ERC20 без механизма предоставления разрешений
- ERC20 — интерфейс ERC20
- BasicToken — абстрактная реализация ERC179
- StandartToken — абстрактная реализация ERC20
- Ownable — предоставляет возможность ограничивать доступ к функциям всем кроме владельца контракта
- SafeMath — библиотека безопасных математических операций. В случае проблем с выполнением операции работа функции прерывается
- MintableToken — предоставляет возможность выпуска новых монет. Если выпуск новых монет больше не нужен, то вызывается finishMinting (в ICO вызывается как правило после окончания распродажи монет). Возобновить выпуск монет больше будет нельзя. Конечно можно было бы дописать функцию, которая разрешает доп.эмиссию монет. Но шаблон скорее всего часто используется в ICO. В случае доп.эмиссии после ICO стоимость монеты может размываться. А это плохо для покупателей нашей монеты. Поэтому если покупатель увидит что после окончания ICO не будет физической возможность выпустить новые монеты, то доверие к нам повысится.
А теперь посмотрите сколько строк кода пришлось бы нам писать с нуля, если бы мы сразу использовали готовые решения:
1 2 3 4 5 6 7 8 9 10 11 |
pragma solidity ^0.4.13; contract SimpleTokenCoin is MintableToken { string public constant name = "Simple Coin Token"; string public constant symbol = "SCT"; uint32 public constant decimals = 18; } |
Все остальное мы бы просто скопировали. А вот полный код:
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 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 |
pragma solidity ^0.4.18; /** * @title ERC20Basic * @dev Simpler version of ERC20 interface * @dev see https://github.com/ethereum/EIPs/issues/179 */ contract ERC20Basic { uint256 public totalSupply; function balanceOf(address who) public constant returns (uint256); function transfer(address to, uint256 value) public returns (bool); event Transfer(address indexed from, address indexed to, uint256 value); } /** * @title ERC20 interface * @dev see https://github.com/ethereum/EIPs/issues/20 */ contract ERC20 is ERC20Basic { function allowance(address owner, address spender) public constant 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); } /** * @title SafeMath * @dev Math operations with safety checks that throw on error */ library SafeMath { function mul(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a * b; assert(a == 0 || c / a == b); return c; } function div(uint256 a, uint256 b) internal pure returns (uint256) { // assert(b > 0); // Solidity automatically throws when dividing by 0 uint256 c = a / b; // assert(a == b * c + a % b); // There is no case in which this doesn't hold return c; } function sub(uint256 a, uint256 b) internal pure returns (uint256) { assert(b <= a); return a - b; } function add(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a + b; assert(c >= a); return c; } } /** * @title Basic token * @dev Basic version of StandardToken, with no allowances. */ contract BasicToken is ERC20Basic { using SafeMath for uint256; mapping(address => uint256) balances; /** * @dev transfer token for a specified address * @param _to The address to transfer to. * @param _value The amount to be transferred. */ function transfer(address _to, uint256 _value) public returns (bool) { require(_to != address(0)); require(_value <= balances[msg.sender]); // SafeMath.sub will throw if there is not enough balance. balances[msg.sender] = balances[msg.sender].sub(_value); balances[_to] = balances[_to].add(_value); Transfer(msg.sender, _to, _value); return true; } /** * @dev Gets the balance of the specified address. * @param _owner The address to query the the balance of. * @return An uint256 representing the amount owned by the passed address. */ function balanceOf(address _owner) public constant returns (uint256 balance) { return balances[_owner]; } } /** * @title Standard ERC20 token * * @dev Implementation of the basic standard token. * @dev https://github.com/ethereum/EIPs/issues/20 * @dev Based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol */ contract StandardToken is ERC20, BasicToken { mapping (address => mapping (address => uint256)) internal allowed; /** * @dev Transfer tokens from one address to another * @param _from address The address which you want to send tokens from * @param _to address The address which you want to transfer to * @param _value uint256 the amount of tokens to be transferred */ 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); Transfer(_from, _to, _value); return true; } /** * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. * * Beware that changing an allowance with this method brings the risk that someone may use both the old * and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this * race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 * @param _spender The address which will spend the funds. * @param _value The amount of tokens to be spent. */ function approve(address _spender, uint256 _value) public returns (bool) { allowed[msg.sender][_spender] = _value; Approval(msg.sender, _spender, _value); return true; } /** * @dev Function to check the amount of tokens that an owner allowed to a spender. * @param _owner address The address which owns the funds. * @param _spender address The address which will spend the funds. * @return A uint256 specifying the amount of tokens still available for the spender. */ function allowance(address _owner, address _spender) public constant returns (uint256 remaining) { return allowed[_owner][_spender]; } /** * approve should be called when allowed[_spender] == 0. To increment * allowed value is better to use this function to avoid 2 calls (and wait until * the first transaction is mined) * From MonolithDAO Token.sol */ function increaseApproval (address _spender, uint _addedValue) public returns (bool success) { allowed[msg.sender][_spender] = allowed[msg.sender][_spender].add(_addedValue); Approval(msg.sender, _spender, allowed[msg.sender][_spender]); return true; } function decreaseApproval (address _spender, uint _subtractedValue) public returns (bool success) { uint oldValue = allowed[msg.sender][_spender]; if (_subtractedValue > oldValue) { allowed[msg.sender][_spender] = 0; } else { allowed[msg.sender][_spender] = oldValue.sub(_subtractedValue); } Approval(msg.sender, _spender, allowed[msg.sender][_spender]); return true; } function () public payable { revert(); } } /** * @title Ownable * @dev The Ownable contract has an owner address, and provides basic authorization control * functions, this simplifies the implementation of "user permissions". */ contract Ownable { address public owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev The Ownable constructor sets the original `owner` of the contract to the sender * account. */ function Ownable() public { owner = msg.sender; } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { require(msg.sender == owner); _; } /** * @dev Allows the current owner to transfer control of the contract to a newOwner. * @param newOwner The address to transfer ownership to. */ function transferOwnership(address newOwner) onlyOwner public { require(newOwner != address(0)); OwnershipTransferred(owner, newOwner); owner = newOwner; } } /** * @title Mintable token * @dev Simple ERC20 Token example, with mintable token creation * @dev Issue: * https://github.com/OpenZeppelin/zeppelin-solidity/issues/120 * Based on code by TokenMarketNet: https://github.com/TokenMarketNet/ico/blob/master/contracts/MintableToken.sol */ contract MintableToken is StandardToken, Ownable { event Mint(address indexed to, uint256 amount); event MintFinished(); bool public mintingFinished = false; address public saleAgent; function setSaleAgent(address newSaleAgnet) public { require(msg.sender == saleAgent || msg.sender == owner); saleAgent = newSaleAgnet; } function mint(address _to, uint256 _amount) public returns (bool) { require(msg.sender == saleAgent && !mintingFinished); totalSupply = totalSupply.add(_amount); balances[_to] = balances[_to].add(_amount); Mint(_to, _amount); return true; } /** * @dev Function to stop minting new tokens. * @return True if the operation was successful. */ function finishMinting() public returns (bool) { require((msg.sender == saleAgent || msg.sender == owner) && !mintingFinished); mintingFinished = true; MintFinished(); return true; } } contract SimpleTokenCoin is MintableToken { string public constant name = "Simple Coin Token"; string public constant symbol = "SCT"; uint32 public constant decimals = 18; } |
В этом уроке мы познакомились с общим подходом написания контрактов монеты ERC20. Научились использовать готовые решения, что сильно упростило нам задачу и избавило от ошибок. Следует отметить что многие контракты написаны так, чтобы избежать уязвимости. Это может быть банальное перемещение двух строк кода, что не всегда очевидно для новичка.
Фактически из этих контрактов-кирпичиков мы можем собрать свой контракт на любой вкус с наименьшими усилиями. На самом деле таких устоявшихся шаблонов проектирования в solidity очень много. В дальнейшем мы познакомимся с некоторыми из них. А пока тут можно найти документацию по самым популярным шаблонам от OpenZeppelin.
Продолжение читать тут. Предыдущий урок тут.
Если у вас возникли вопросы то можете смело писать на электронную почту (раздел «контакты«). Также приветствуется критика.
Если статья показалась вам полезной и вы желаете отблагодарить автора, то это можно сделать отослав немного эфира на адрес 0xEA15Adb66DC92a4BbCcC8Bf32fd25E2e86a2A770.
Подскажите пжс, а что значит такое сообщение при создании токена?
Такое же сообщение выскакивает при запуске моего токена…
creation of browser/ballot.sol:SimpleTokenCoin pending…
[vm] from:0xca3…a733c, to:browser/ballot.sol:SimpleTokenCoin.(constructor), value:0 wei, data:0x606…e0029, 0 logs, hash:0x3eb…c694a
Details
Debug
undefined errored: Cannot read property ‘op’ of undefined
undefined errored: Cannot read property ‘op’ of undefined
undefined errored: Cannot read property ‘op’ of undefined
undefined errored: Cannot read property ‘op’ of undefined
undefined errored: Cannot read property ‘op’ of undefined
undefined errored: Cannot read property ‘op’ of undefined
Это специфика Remix. В основном на такие ошибки обращать не стоит. Remix недавно обновился и поэтому есть некоторые проблемы.
Доброго времени суток.
А как подключить в Remix библиотеки (в том числе OpenZeppelin)
При попытке компиляции Вашего примера, выдаёт:
browser/ballot.sol:3:29: DeclarationError: Identifier not found or not unique.
contract SimpleTokenCoin is MintableToken {
Подчёркивая MintableToken.
Понимаю, что не видит OpenZeppelin. Но как подключить не пойму ((
Кажется нашёл.
Надо добавить в начале
import «github.com/OpenZeppelin/zeppelin-solidity/contracts/token/MintableToken.sol»;
Вопрос подключения внешних библиотек (файлов) совсем не описан в рунете. ((
Мелочь, но вгоняет ступор.
Такая запись так же подгружает всё на что MintableToken.sol ссылается внутри.
Реально полезная находка! Спасибо огромное!
я не смогла подключить данным способом. Пишет:
Error: Could not find github.com/OpenZeppelin/zeppelin-solidity/contracts/token/MintableToken.sol from any sources; imported from /Users/…/SimpleTokenCoin.sol
_____
Truffle v3.4.11 (core: 3.4.11)
Solidity v0.4.15 (solc-js)
___
кто-нибудь может помочь с этим?
При компиляции в ремиксе выдает ошибки:
1. Для функции transferFromParserError:
Expected token LParen got ‘Identifier’
и следующей ParserError: Expected identifier, got ‘LBrace’
2. Для contract Ownable
ParserError: expected primary expression
В чем может быть проблема?
Тоже запарился с этой проблемой )
Вот тут выложил решение https://gitlab.com/snippets/1686495
PS. Автор, пожалуйста прорефакторь код, у тебя там слиплось в одну строку более одной функции
Спасибо, поправил.
Спасибо, поправил.
а почему из кода убрали StandartToken от Zeppelin и функцию transferFrom перенесли в basiktoken
Доброго времени суток.
Правильно, ли я понимаю, что функцию finishMinting() нужно будет вызвать вручную после окончания ICO (к примеру)?
И вызывать ее нужно будет с адреса смарт-контракта.
У вас тут опечатка:
saleAgent = newSaleAgnet;
Что делает команда assert? И не могли бы вы рассказать про address indexed и internal pure returns, чем они от обычных отличаются?
Здравствуйте, а нет случайно уже готового контракта вообще без функций и всяких остальных заморочек? Просто хочу лярд erc20 монеток на моем кошельке с моим названием+сокращением+лого…
Ну и естественно чтоб их можно было отправить без заморочек другу, а он своим друзьям и так далее…
???
А какой контракт нужно выбрать чтобы весь опубликовался в блокчейне или это не так важно? «basic token»,»erc20″,»erc20basic»,»mintable» и тд.
Все понял, извиняюсь за тупой вопрос)))
Такой вопрос, почему скопировав данный код в теме, я не могу переправлять монеты функцией TransferFrom?
Права и количество функцией approve предоставил в трее выдало «true», но выдает ошибку при попытке вызова функции TranferFrom
Кто разрабатывает смарт-контракты на ERC20, .., ICO. Нужно сделать контракт. Скайп Vist36, sarelav@ya.ru