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

в 20:56, , рубрики: ico, javascript, node.js, solidity, блокчейн, Криптовалюты, платежные системы, попрошайничество, сбор денег, теги никто не читает

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

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

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

Solidity

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

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

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

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

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

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

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

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

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

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

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

Первое, что мы сразу поставим и запустим процесс синхронизации блоков — это geth. Короче, это утилита, написанная на Go, которая позволит нам запускать ноду эфира на локальном компе и коннектиться к тестовой и реальной сети. Установить можно через инсталяторы, но я крайне советую фигачить 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 — они молодцы.

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

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 {
 constructor(
  uint256 _openingTime,
  uint256 _closingTime,
  uint256 _rate,
  address _wallet,
  uint256 _cap,
  MintableToken _token,
  uint256 _goal
 )
  public
  Crowdsale(_rate, _wallet, _token)
  CappedCrowdsale(_cap)
  TimedCrowdsale(_openingTime, _closingTime)
  RefundableCrowdsale(_goal)
 {
  // Это тут просто, чтобы показать, что можно сделать
  // Проверяем, что софткеп ниже хардкепа
  require(_goal <= _cap);
 }
}

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

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

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

Миграции

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

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

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

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

Теперь открываем новое окошко Терминала (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);
  }

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

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

Автор: backmeupplz

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js