Сделать токен в Ethereum — значит развернуть смарт-контракт по стандарту ERC-20. Писать Solidity с нуля не нужно: мастер OpenZeppelin Contracts Wizard собирает контракт по выбранным параметрам, а Remix компилирует его и отправляет в сеть. После одной транзакции у токена появляется адрес, балансы и правила, которые уже нельзя изменить.

Токен в Ethereum — это контракт, а не «монета»

Стандарт ERC-20 задаёт набор функций и событий: name, symbol, decimals, totalSupply, balanceOf, transfer, transferFrom, approve, allowance, а также события Transfer и Approval. Кошельки, обозреватели и пулы работают с любым контрактом, который реализует этот интерфейс.

Развёртывание — обычная транзакция, которая создаёт контракт с собственным адресом. Дальше всё решают правила внутри контракта: он ведёт учёт токенов, но не умеет сам их продавать, не формирует цену и не создаёт ликвидность. Это отдельные задачи.

Всё ниже — только про Ethereum и ERC-20: в других сетях свои стандарты, инструменты и комиссии.

Что выбрать до развёртывания

Параметры конструктора определяют правила для всех держателей, поэтому это не галочки, а решения.

ПараметрТипичный выборЧто это меняет
Name и SymbolGold и GLDЗадаются один раз. Опечатка в символе останется навсегда.
Decimals18, как у ETH; 0 — если делить токен не нужноТолько формат отображения. При 0 отправить 1,5 токена нельзя. После деплоя не меняется.
Premint (начальная эмиссия)1 000 000Сумма сразу зачисляется адресу деплоера и входит в totalSupply.
Mint (допэмиссия)Выключен, если предложение должно быть фиксированнымВладелец может выпускать новые токены и увеличивать предложение.
BurnableПо желаниюДержатель может сжечь свои токены, totalSupply уменьшается.
PausableТолько если нужен рубильникВладелец может остановить все переводы, включая вывод средств держателями.
Freeze или blacklist (доп. код)Только если действительно нужноВладелец блокирует исходящие переводы конкретного адреса. Галочки в мастере нет — функцию добавляют вручную.
ERC20VotesДля голосованийДобавляет делегирование голосов и учёт истории балансов.
Access ControlOwnable для одного владельца; роли — если прав несколькоОпределяет, кто распоряжается административными функциями.
UpgradabilityNoneКонтракт неизменяем. Прокси-обновления усложняют проверку и добавляют админа.

Premint вместе с mint означает, что предложение контролируете вы: сначала выпускаете 1 000 000, потом можете добавить ещё. Pause и freeze дают власть над чужими переводами, и чем больше таких прав, тем сильнее держатели зависят от адреса, который ими управляет. Для токена, который должен жить долго, права владельца разумнее передать мультисигу: тогда для mint или pause нужно согласие нескольких людей. Список прав владельца публикуйте вместе с адресом токена — именно его будут проверять покупатели.

Развёртывание в Remix по шагам

  1. Откройте OpenZeppelin Contracts Wizard и выберите ERC20.

  2. Заполните Name и Symbol (Gold и GLD), укажите Premint 1 000 000 в целых токенах. В Features снимите Permit; если предложение должно быть фиксированным, снимите и Mintable. Access Control — Ownable, Upgradability — None.

  3. Нажмите Open in Remix (или Download, если работаете в другой среде). Контракт и зависимости появятся в проекте.

  4. Скомпилируйте контракт в Remix.

  5. На вкладке развёртывания выберите сеть и аккаунт, на котором есть ETH для газа, и подтвердите транзакцию в кошельке.

  6. Ожидаемый результат: адрес контракта и 1 000 000 GLD на адресе деплоера. Функции balanceOf и totalSupply вернут 10²⁴ минимальных единиц.

  7. Если транзакция отклонена, чаще всего не хватает ETH на газ или выбрана не та сеть. Контракт, развёрнутый в тестовой сети, остаётся там: в основную сеть он не «переедет».

  8. Сразу после успешного деплоя опубликуйте исходник и версию компилятора в обозревателе — без этого контракт никто не сможет проверить.

Пример: 1 000 000 GLD и перевод 1,5 GLD

Параметры примера: Gold, GLD, decimals 18, premint 1 000 000, владелец — Ownable, без Permit. Solidity и EVM не хранят дробные числа, поэтому внутри контракта количество записывается целым числом минимальных единиц: 1 000 000 × 10¹⁸ = 10²⁴. Именно это вернёт balanceOf деплоера, а кошелёк покажет 1 000 000 GLD, разделив значение на 10¹⁸.

Чтобы отправить 1,5 GLD, контракт вызывают с суммой 1,5 × 10¹⁸ = 1 500 000 000 000 000 000. Decimals на арифметику не влияют: все переводы идут целыми числами, а интерфейсы только корректируют отображение. В документации OpenZeppelin тот же механизм показан на 1000 токенов: balanceOf вернёт 10²¹ единиц, а перевод 300 токенов — это transfer(recipient, 300000000000000000000).

Защитные проверки, чтобы держатели не теряли токены

Стандарт ERC-20 не сообщает контракту-получателю о входящем переводе. Если контракт не умеет обрабатывать токены, они остаются на его адресе: по оценке ethereum.org на 20 июня 2024 так потеряно не менее $83,7 млн. Чистая реализация стандарта к этому склонна, если не добавить ограничения сверху.

Два частых случая ошибки — перевод на адрес самого токен-контракта и на пустой адрес, который никому не принадлежит. В OpenZeppelin v5 любое изменение баланса — перевод, выпуск и сжигание — проходит через внутреннюю функцию _update. Её переопределяют в туториале ethereum.org, чтобы проверки срабатывали до изменения балансов:

function _update(address from, address to, uint256 value) internal virtual override(ERC20) {
    // Сжигание отправляет токены на address(0), его не блокируем
    if (to != address(0)) {
        require(to != address(this), "Can't send tokens to the contract address");
        bool isToContract;
        assembly { isToContract := gt(extcodesize(to), 0) }
        require(to.balance != 0 || isToContract, "Can't send tokens to an empty address");
    }
    super._update(from, to, value);
}

Что это даёт: перевод на адрес контракта и на адрес, у которого нет ни ETH, ни кода, отклоняется. Второе правило — эвристика, и у неё есть цена: новый кошелёк, на который ещё не приходило ETH, тоже будет отклонён, пока получатель не пополнит его небольшим количеством ETH. После EIP-7702 код может быть и у обычного аккаунта, поэтому проверка определяет наличие кода, а не строго тип аккаунта.

Блокировать переводы на все контракты нельзя: это ломает совместимость с пулами ликвидности, которые принимают токены через approve и transferFrom. Поэтому пополняйте контракты связкой approve + transferFrom, а не прямым transfer.

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

Публикация исходника и проверка чужого токена

Публикация исходника — не формальность: обозреватель компилирует предоставленный код независимо и отклоняет его, если байткод не совпал с развёрнутым. Совпавший исходник — единственный способ для стороннего человека убедиться, что контракт делает заявленное.

Дальше смотрят список функций владельца (mint, burn, pause, freeze, cleanup) и события: при переводе, выпуске и сжигании должен эмититься Transfer, при одобрении — Approval. Кошельки узнают о движении токенов именно по событиям.

В разборе ethereum.org на примере wARB, который выдавал себя за ARB, видны типичные приёмы: событие Transfer без реального изменения балансов, функция-двойник Approve с заглавной буквы, которая сжигает балансы держателей, и выпуск токенов владельцу с событием о зачислении другому адресу.

Автоматическая проверка событий помогает, но даёт ложные отрицания: совершенно нормальный ERC-20 может не представлять ничего реального. Поэтому адрес настоящего токена берите из документации эмитента, а не из объявления или сообщения.

Что дальше

После деплоя у вас есть адрес контракта и возможность переводить токены — этого достаточно для внутренней экономики, голосований и тестов. Для обращения на рынке понадобятся пул ликвидности и раскрытие прав владельца; это отдельная работа, и развёрнутый контракт её не выполняет.