Главная » Хабрахабр » Как написать смарт-контракт для ICO за 5 минут

Как написать смарт-контракт для ICO за 5 минут

В этой статье я расскажу вам, как за 5 минут и несколько команд в терминале запустить смарт-контракт сбора денег для своего ICO на Ethereum. Всем привет! Вкратце, на этот смарт-контракт можно будет отправить денег и получить за это ERC20 токены. Этот очерк потенциально сэкономит вам десятки тысяч американских долларов, так как любой программист — да и не программист тоже — сможет запустить проаудированный и безопасный смарт-контракт (вместо того, чтобы платить $15,000 – $75,000 за разработку). Можно сказать, эта статья — сборник всего опыта, который я получил, запуская ICO для своего проекта.

К слову, чтобы эта статья оставалась актуальной, постараюсь указать потенциальные места, где она может устареть (и как это поправить). В Интернетах этих ваших и так полно статьей про смарт-контракты, но как только начинаешь писать оный, сталкиваешься с тем, что информация везде повторяется, а туториалов, как запулить свой ERC20 попросту либо нет, либо они устарели что аж донельзя. Поехали!

Solidity

Это название главного языка, который разработала команда кефира для запуска смарт-контрактов. Если вы программист, то просто пробегитесь глазами по документации языка — он неприлично простой. К слову, сделали его простым, чтобы было сложнее ошибиться в написании смарт-контракта. Так что абсолютно любой программист хотя бы уровня джуниора сможет разобраться в нем. Абсолютно нет смысла платить огромные деньги разработчикам, которые знают солидити — обучить уже существующего разработчика будет на порядок дешевле.

Смарт-контракты

… и все, что вам нужно о них знать. Пропустите эту секцию, если вы не программист. Смарт-контракт — это кусок кода. В принципе, это класс в солидити (ООП, да), у которого есть два типа функций: изменяющие состояние и не изменяющие. Ну и чтобы запускать функции в смарт-контракте просто отправив на него кефир, нужно эту функции пометить payable.

Контракты могут изменять блокчейн (состояние, хранилище) — но чтобы изменить блокчейн нужно заплатить кефира майнерам. Состояние — это хранилище данных, блокчейн, епта. Оплата майнерам за запуск кода, изменяющий состояние, называется Газом (Gas). Как они будут делить кефир разбирать не будем в рамках этой статьи. Обычно в ERC20 токенах это функции, которые либо выдают отправителю токенов за кефир, либо переводят токены от одного держателя токенов другому. Если кто-то извне закинет кефира на адрес смарт-контракта с вызовом функции, помеченной payable, но не помеченной Constant, View или Pure, то из отправленной суммы будет вычтено нужное количество кефира для оплаты майнерам.

Даже больше скажу, эти функции не нужно вызывать транзакциями — ведь любой клиент кефира, теоретически, сможет ее выполнить у себя — и никому больше об этом знать не нужно (в блокчейн ведь ничего не пишется). А если вы пометите функцию в контракте словами Constant или View (означают одно и то же, разрешают только читать состояние), либо Pure (то же самое, только даже состояние не читает), то на исполнение этой функции даже кефир тратить не нужно будет!

Про них тоже нужно знать. А еще есть две важные штуки в солидити: множественное наследование и модификаторы функций.

Первое — просто контракты могут наследоваться одновременно с нескольких классов типа TimedCrowdsale, CappedCrowdsale, MintedCrowdsale, Ownable — при этом функции конструкторов тоже запускаются друг за другом — но это я объясню на примере уже дальше.

Это как простая инкапсуляция, только чуть более гибкая — это буквально шаблон функции. Второе — это возможности создавать функции, которые потом будут вставлены в другие функции. То есть модификаторы — это не просто инкапсулированный функционал, который возвращает значение; это — шаблон функции, когда код из модификатора буквально вставляется в функцию, использующую этот модификатор. Когда вы создаете модификатор, вы пишете специальный символ _ там, где подразумеваете код функции, использующей этот модификатор.

Перейдем к практике.

Готовим окружение

Если вы не знаете, что такое Терминал — почитайте вот эту статью. Если вы на окнах, ставьте себе Терминал через WLS. Если вы уже знакомы с Терминалом, продолжим. Алсо, сразу поставьте себе Node.js — он будет необходим для следующих шагов. Лучше ставить LTS, но, на самом деле, абсолютно без разницы, какую из современных версий ноды ставить.

Короче, это утилита, написанная на Go, которая позволит нам запускать ноду эфира на локальном компе и коннектиться к тестовой и реальной сети. Первое, что мы сразу поставим и запустим процесс синхронизации блоков — это geth. Проверить, установился ли у вас норм geth, можно, запустив команду в Терминале: Установить можно через инсталяторы, но я крайне советую фигачить geth сразу в Терминал, как описано вот тут.

geth version

Если вам выплюнуло версию geth — все в ажуре, продолжаем туториал. Если нет — хреново, исправляйте; придется, похоже, заняться любовными ласками с Терминалом и своей операционной системой — но вам не впервой, разберетесь. Как установите geth, запускайте в Терминале команду:

geth --testnet console

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

eth.blockNumber # если 0 — то еще не синхронизировались
eth.syncing # выплюнет прогресс синхронизации или false, если ничего не происходит

Процесс синхронизации у меня занимал от 1 до 4 часов — когда как. Алсо, помимо синхронизации блоков, придется ждать еще и синхронизации состояний — это чаще дольше, чем синхронизация блоков. Также можно использовать команды geth с флагом --light — тогда синхронизация длится от нескольких секунд до минуты и вы все еще можете деплоить контракты.

Нам нужно поставить аналог geth, только совсем уж локальную симуляцию блокчейна — testrpc. Ладно, первую утилиту мы поставили — ставим следующую. Да-да, у нас 3 блокчейна:

  • testrpc — локальная симуляция блокчейна; быстрая, но ненастоящяя и хранится только у вас на машине
  • geth --testnet — уже реальный блокчейн, но тестовая сеть, где можно бесплатно получать кефир и тестить всякие непотребства, денег не потеряете
  • geth — мейннет, главный, реальный блокчейн, настоящий кефир; все по-взрослому, ошибки тут — потери реального кефира

Соответственно, начнем мы тест контрактов с testrpc, потом задеплоим в geth --testnet, а потом зафигачим прямо в geth.

Ставим testrpc, запустив следующую команду:

npm install -g ethereumjs-testrpc

Ну или встанет сразу с трюфелем, так как теперь testrpc под крылом трюфеля и зовется ganache-cli. Хотя черт его знает, у меня все и с ванильным testrpc сработало. А если работает — не трогай, как учили меня в межгалактической академии. Можно еще его и запустить, чтобы проверить установку, прописав truffle в консоли, но у нас уже синхронизируется тестовый блокчейн — не будем ему мешать.

Ноды теперь есть и тестовая даже синхронизируется? Ну что, разобрались с блокчейнами? Ставим удобную утилиту для работы со смарт-контрактами на кефире — truffle, следующей командой:

npm install -g truffle
truffle version # сразу чекнем, установился ли, проверив версию

Трюфель — это тулза, которая позволяет держать смарт-контракты в разных файлах, импортировать другие файлы, а так же компилирует ваш код смарт-контрактов в один большой байт-код (нечитаемый человеком), автоматически находит у вас локально запущенный geth (тестовый и реальный) или testrpc, деплоит ваш смарт-контракт в эту сеть. Алсо, проверяет ваш код смарт-контракта на ошибки и с недавних пор еще и дебажить помогает завершенные транзакции. Мастхев, короче.

На этом этапе у вас должны быть установлены: testrpc, geth, truffle — если чего-то из этого нет или версия не выплевывается в консоль по запросу, то поправьте это; иначе не получится у вас ничего.

Вызывается вот так: Алсо, я накидал простенький баш-скриптик, который установит все за вас.

source <(curl -s https://raw.githubusercontent.com/backmeupplz/eth-installer/master/install.sh)

— но я его ни разу не тестил еще, так что не уверен в его работоспособности. Однако пулл-реквестам буду только рад.

Фигачим контракт

За вас все уже придумано и написано — это хорошо. Немного головняка будет все равно — но я постараюсь вам его минимизировать. Использовать мы будем уже готовые ERC20 контракты от OpenZeppelin — это сейчас стандарт индустрии, они прошли аудит, да и вообще все их код используют. Спасибо им огромное за вклад в опенсоус.

Сделайте cd в какую-нибудь безопасную папку и после пропишите:

mkdir contract && cd contract

В этой папке и будем работать. Создадим здесь заглушку для нашего смарт-контракта:

truffle init

Зашибись, четко. У нас теперь есть две очень важные папки, в которые мы и будем лезть: contracts и migrations. Первая — код наших контрактов, вторая — код для truffle, чтобы знать, что делать при деплое контрактов в блокчейн.

Дальше нам нужно забрать текущий код смарт-контрактов из npm и, собственно говоря, начать сам проект:

npm init -y # создадим проект без вопросов (флаг -y)
npm install -E openzeppelin-solidity # заберем контракты и зафиксируем текущую версию (флаг -E)

Отлично, код смарт-контрактов от OpenZeppelin у нас в кармане в папке node_modules/openzeppelin-solidity/contracts. Теперь заходим в главную папку contracts, удаляем там все файлы и добавляем файлы MyToken.sol и MyCrowdsale.sol — естественно, свои контракты вы назовете иначе. Первый будет контрактом на наш ERC20 Токен, а второй — контрактом нашего ICO, который будет принимать кефир и раздавать людям MyToken. Эта статья может устареть, но вы всегда можете глянуть, как OpenZeppelin предлагают вам создавать контракты у них в репозитории. Вот так у нас будет выглядеть MyToken.sol:

pragma solidity ^0.4.23; // Imports
import "../node_modules/openzeppelin-solidity/contracts/token/ERC20/MintableToken.sol"; // Main token smart contract
contract MyToken is MintableToken { string public constant name = "My Token"; string public constant symbol = "MTKN"; uint8 public constant decimals = 18;
}

Найс — у вас есть смарт-контракт собственного токена (только смените названия в константах)! Можете глянуть, что там за наследование от MintableToken — но там все максимально просто. Это токен, который можно выпускать (от англ. «Mint» — чеканить), и выпускать его имеет право только владелец, так как MintableToken еще и наследуется от Ownable. Алсо, MintableToken еще и наследуется от классов ERC20 токенов, написанных OpenZeppelin, в которых и реализован интерфейс ERC20:

contract ERC20Basic { 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);
}

Ага, вот вам и весь ERC20 интерфейс. Сложно? Не думаю. Дает возможность глянуть, сколько было выпущено токенов, проверить баланс адреса и перевести токенов на другой адрес, выплюнув в сеть событие перевода для легких клиентов кефира. И все это вы получаете фор фри в вашем MyToken.sol благодаря работе OpenZeppelin — они молодцы.

Вот так будет выглядеть ваш MyCrowdsale.sol: А теперь перейдем к главной части нашего ICO — нам же нужно принимать кефиры и раздавать MyToken!

pragma solidity ^0.4.23; // Imports
import "../node_modules/openzeppelin-solidity/contracts/crowdsale/emission/MintedCrowdsale.sol";
import "../node_modules/openzeppelin-solidity/contracts/crowdsale/distribution/RefundableCrowdsale.sol";
import "../node_modules/openzeppelin-solidity/contracts/crowdsale/validation/CappedCrowdsale.sol";
import "../node_modules/openzeppelin-solidity/contracts/token/ERC20/MintableToken.sol"; contract MyCrowdsale is CappedCrowdsale, RefundableCrowdsale, MintedCrowdsale
}

Так-так-так, что тут у нас? Что, пацаны, смарт-контракты? Наша публичная продажа токенов наследует три самых популярных свойства: у нее есть хард-кап, больше которого собрать не получится; софт-кап, не собрав который эфиры возвращаются; время начало и конца продажи токенов. Собственно говоря, а что еще для счастья нужно?

Алсо, мы проверяем, что хардкеп у нас выше софткепа — алес гут! Программисты, обратите внимание, как конструкторы классов множественного наследования выстроены в ряд и получают аргументы из главного конструктора MyCrowdsale. Алсо, не пугайтесь туче параметров в конструкторе MyCrowdsale — мы передадим их на этапе деплоя контракта в трюфеле.

Алсо, его поддерживают все ERC20 кошельки — ляпота! Вот и все — у вас есть готовые контракты вашего собственного ERC20 токена и даже смарт-контракт ICO, который настраивается по вашему желанию и раздает ваши токены за кефир. Перейдем к ручным тестам и деплою.

Миграции

Как я уже говорил ранее, тестировать мы будем последовательно на трех блокчейн-сетях, но процесс тестирования ручками всегда будет один и тот же. Начнем с testrpc, потом перейдем к geth --testnet и дальше в geth. Соу фар мы только писали код, давайте его попробуем скомпилировать. В папке проекта пропишите:

truffle compile

Если все скомпилировалось без проблем — то у вас появится папочка build, в которой будет содержаться кракозябра для трюфеля, чтобы он смог задеплоить в блокчейн байт-код ваших смарт-контрактов. Перед тем, как деплоить смарт-контракты, нам нужно рассказать трюфелю, что вообще нужно делать. Деплой смарт-контрактов в трюфеле называется миграцией — ну, что же, будем придерживаться этой терминологии. Зайдите в migrations/1_initial_migration.js и измените его следующим способом:

const token = artifacts.require("../contracts/MyToken.sol");
const crowdsale = artifacts.require("../contracts/MyCrowdsale.sol"); module.exports = function(deployer, network, accounts) { const openingTime = 1514764800; // 15 Июня 2018 const closingTime = 1561939200; // 1 Июля 2019 const rate = new web3.BigNumber(1); // 1 токен за 1 эфир const wallet = '0x281055afc982d96fab65b3a49cac8b878184cb16'; // Кошелек-бенефициар const cap = 200 * 1000000; // Хардкеп const goal = 100 * 1000000; // Софткеп return deployer .then(() => { return deployer.deploy(token); }) .then(() => { return deployer.deploy( crowdsale, openingTime, closingTime, rate, wallet, cap, token.address, goal ); }) .then(() => { // Crowdsale должен владеть токеном var tokenContract = web3.eth.contract(token.abi).at(token.address); web3.eth.defaultAccount = web3.eth.accounts[0]; tokenContract.transferOwnership(crowdsale.address); });
};

Это тот самый файл, который будет использоваться трюфелем для деплоя контрактов. Что же мы тут такого наворотили? Во-первых, мы запросили скомпилированные MyToken и MyCrowdsale. После, мы установили константы со всеми аргументами нашего ICO — установили время начала и конца; сколько токенов будут получать люди за 1 вей кефира (0.000000000000000001 eth = 1 wei; установка decimals указывает, сколько нужно порядков wei, чтобы получить 1 ваш новоиспеченный токен); кошелек, куда придут полученные на продаже кефиры; хард-кеп и софт-кеп. Учтите, что openingTime всегда должен быть после времени текущего блока в блокчейне — иначе ваш смарт-контракт не задеплоится из-за проверки условия в TimedCrowdsale. Я на эти грабли наступал, а провалившиеся транзакции вообще никак не получается дебажить. Меняйте эти константы по своему усмотрению.

Тут ничего интересного: у нас есть объект deployer, который деплоит артефакты смарт-контрактов и передает туда аргументы. Следующий шаг — это именно деплой смарт-контрактов. Заметьте, что сначала деплоится MyToken, и только потом MyCrowdsale — и во второй передается аргументом адрес первого.

Когда вы создаете с кошелька MyToken, этот кошелек становится владельцем MyToken по суперклассу Ownable — то же самое происходит и с MyCrowdsale. Дальше самое интересное — то, о чем не пишут ни в документации, ни в книжках. А кто владелец MyToken? Если глубоко копнуть в MintableToken, то можно увидеть, что чеканить монеты-то может только Owner! А кто будет отправлять запросы на чеканку монет? Правильно: адрес, который его и задеплоил. Напомню, что адрес, создавший MyToken и адрес MyCrowdsale — это два разных адреса. Правильно: смарт-контракт MyCrowdsale.

А MyCrowdsale все еще под владением web3.eth.accounts[0] — так что все пучком. Поэтому у нас добавляется неправославный третий шаг деплоя, где адрес, задеплоивший контракты (web3.eth.accounts[0]) вызывает функцию transferOwnership на контракте MyToken, чтобы MyCrowdsale владел MyToken и мог чеканить монеты.

Заметка про web3.eth.accounts[0]: когда деплоите смарт-контракт, убедитесь, что geth или testrpc имеют правильный кошелек в web3.eth.accounts[0] — не теряйте приватный ключ к нему, хоть это никак вам не навредит, но вдруг владельцу что-нибудь потом нужно будет сделать, а ключа уже нет?

Не теряйте пароль и приватные ключи. В testrpc, как правило, аккаунты создаются сразу при запуске и они сразу же разлочиваются; однако на тестовом и реальном блокчейне эфира стоит создать аккаунт через personal.newAccount() — дальше пополнить этот адрес через Faucet на тестовом блокчейне или реальным кефиром на реальном блокчейне.

Думаю, разберетесь. Алсо, вы можете в аккаунты добавить уже существующий кошелек, вызвав web3.personal.importRawKey('pvt_key', 'password'), но для этого нужно вызывать geth с дополнительным параметром --rpcapi="db,eth,net,web3,personal,web3".

Тестирование и деплой

Йес, контракты готовы, миграции написаны, осталось только задеплоить и проверить. Как geth (тестовый и реальный), так и testrpc управляются одинаково через truffle console — так что опишу способ проверки для testrpc и просто расскажу, как включить geth после. И так, запускаем тестовый локальный блокчейн кефира:

testrpc

Эм… вот и все. У вас локально работает симуляция блокчейна кефира.

А чтобы задеплоить в реальный блокчейн эфира, вы пропишите просто geth --rpc. А чтобы задеплоить в тестовый блокчейн эфира, вы вместо этой команды сделаете geth --testnet --rpc. Следующие шаги деплоя и теста более-менее одинаковы для всех трех типов блокчейна. Флаг --rpc нужен, чтобы трюфель смог подключиться. Ремарка про эта была в самом начале статьи. Единственное что — после того, как вы запустите тестовый или реальный блокчейн через geth, он начнет синхронизировать блоки — а это может занять до 4-5 часов на хорошем Интернет-соединении. Алсо, блокчейн весит в районе 60-100 гигабайт, так что подготовьте для этого место на диске. Перед деплоем смарт-контрактов рекомендую дождаться полной синхронизации.

Обычно можно прописать в консоли testrpc, которая открывается сразу, либо в отдельном окошке Терминала в консоли, которая открывается через geth console: eth.unlockAccount(eth.accounts[0], "Пароль, полученный при создании учетки", 24*3600) — это разлочит ваш аккаунт, который должен создать смарт-контракт Алсо-алсо, убедитесь, что web3.eth.accounts[0] разлочен.

Теперь открываем новое окошко Терминала (testrpc не закрываем — он должен работать) и прописываем в папке проекта:

truffle migrate --reset

Эта магическая команда скомпилирует смарт-контракт (то есть не нужно каждый раз писать truffle compile) и задеплоит его на микро-сервер блокчейна, найденный открытым локально. Стоит отметить, что если testrpc сделает это мгновенно, то тестовый и реальный блокчейны будут гораздо дольше включать транзакцию в следующие блоки. После этого у вас должно выплюнуться нечто подобное в консольку:

Using network 'development'. Running migration: 1_initial_migration.js Running step... Replacing MyToken... ... 0x86a7090b0a279f8befc95b38fa8bee6918df30928dda0a3c48416454e2082b65 MyToken: 0x2dc35f255e56f06bd2935f5a49a0033548d85477 Replacing MyCrowdsale... ... 0xf0aab5d550f363478ac426dc2aff570302a576282c6c2c4e91205a7a3dea5d72 MyCrowdsale: 0xaac611907f12d5ebe89648d6459c1c81eca78151 ... 0x459303aa0b79be2dc2c8041dd48493f2d0e109fac19588f50c0ac664f34c7e30
Saving artifacts...

Думаю, вы уже поняли, что консолька вам выдала адреса смарт-контрактов MyToken и MyCrowdsale. Все! Смарт-контракт задеплоен в тот блокчейн, микро-сервер которого у вас открыт. Осталось лишь проверить, что токены и вправду раздаются юзерам, которые присылают кефир на смарт-контракт MyCrowdsale. Прописываем в Терминале следующее, чтобы зайти в консоль трюфеля:

truffle console

Прописываем следующее в теперь уже трюфеле (без комментариев только):

// Сохраняем адреса смарт-контрактов
t="0x2dc35f255e56f06bd2935f5a49a0033548d85477" // Замените на адрес своего MyToken
с="0xaac611907f12d5ebe89648d6459c1c81eca78151" // Замените на адрес своего MyCrowdsale // Получаем инстансы смарт-контрактов
token=MyToken.at(t)
crowdsale=MyCrowdsale.at(c) // Сохраним аккаунт в более короткое имя
account=web3.eth.accounts[0] // Проверяем, сколько токенов у нашего аккаунта
token.balanceOf(account) // должно быть 0 // Отправляем кефира на смарт-контракт
web3.eth.sendTransaction({from: account, to:c, value: web3.toWei(0.1, 'ether'), gas: 900000})

В случае с testrpc можно сразу же проверять снова баланс нашего кошелька, но в случае с тестовым и реальным блокчейном нужно подождать, пока транзакция наша будет включена в блок — обычно, когда это происходит, трюфель выдает вам номер транзакции. Подождали? Проверяем снова наш баланс в MyToken:

// Проверяем, сколько токенов у нашего аккаунта
token.balanceOf(account) // должно быть больше нуля

Вот и все! Сначала тестите свой контракт на testrpc, потом на geth --testnet, потом деплойте на geth. Вот и запустили вы свое собственное ICO! И не пришлось вам тратить десятки килобаксов на аудит и запуск. Накосячить с тем, что нам предоставили ребята из OpenZeppelin, на самом деле, очень сложно. А когда вы используете truffle — так разработка на солидити вообще в сказку превращается. Ну, кроме случаев, когда транзакции ревертятся еще во время выполнения на смарт-контракте — дебажить их сущий ад. Но дебаггинг смарт-контрактов, воистину, достоин отдельной статьи.

Заключение

Огромное спасибо, что дочитали до конца этой статьи! Если мне удалось сэкономить вам время или деньги, либо если вы узнали что-то новое из этой статьи, то я этому буду очень рад. Буду так же очень признателен, если поделитесь этой статьей со своими друзьями или знакомыми, которые хотят провести ICO — сэкономьте им $75,000 на недо-программистов, которые высасывают деньги из крипто-рынка, как паразиты, копи-пастя одни и те же 25 строк кода.

Остались вопросы? Удачи в разработке смарт-контрактов! Милости прошу в комментарии — с удовольствием на все отвечу и постараюсь помочь с проблемами.

Бонус

А что, если вы хотите изменить логику, по которой считается цена покупки токенов? Конечно, можно изменить правильно rate или использовать один из классов контрактов от OpenZeppelin, но вдруг вы хотите чего-нибудь еще более извращенного? В смарт-контракте можно оверрайтнуть функцию getTokenAmount следующим образом:

function _getTokenAmount(uint256 _weiAmount) internal view returns (uint256) { if (block.timestamp < 1533081600) { // August 1st, 2018 rate = rate * 4; } else if (block.timestamp < 1546300800) { // January 1st, 2019 rate = rate * 2; } return _weiAmount.mul(rate); }

В общем, так можно сделать цену токена зависящей от времени покупки — чем дальше в лес, тем дороже токены. Не бойтесь экспериментировать и переписывать некоторые функции смарт-контрактов — это весело!


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Перевод] Ленивая загрузка изображений с использованием IntersectionObserver

В наши дни главным камнем преткновения на пути к высокой скорости загрузки сайтов являются изображения. Это особенно характерно для проектов из сферы электронной коммерции. На них изображения, обычно довольно «тяжёлые», составляют основную часть содержимого страниц. Это, как правило, приводит к ...

Два скилла, которые помогут стать отличным разработчиком

В новом своем материале он решил рассказать, какие навыки помогают разработчику в его ежедневном труде. От переводчика: эта статья — перевод оригинальной статьи Бара Франека, специалиста по JavaScript. Но речь не о программных инструментах, а, скорее, ментальных. Без разницы, какую ...