ERC-20 с нуля: пишем токен на Solidity и разбираем каждую строку
Минимальная реализация стандарта ERC-20 на Solidity: балансы, переводы, allowance, события, типичные ошибки и почему в продакшене нужны проверенные библиотеки.
В этой статье
Токен ERC-20 кажется магией, пока не увидишь исходный код: это маленький контракт с двумя таблицами и тремя функциями. Здесь мы напишем его целиком и разберём каждую часть. Понятийный обзор - в статье токены и NFT, механика approve - в отдельном разборе. Синтаксис - в обзоре Solidity.
Что требует стандарт
ERC-20 определяет интерфейс: функции totalSupply, balanceOf, transfer, approve, allowance, transferFrom и события Transfer, Approval. Необязательные: name, symbol, decimals.
Полный код
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
contract SimpleToken {
string public name = "Simple Token";
string public symbol = "SIMP";
uint8 public constant decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(uint256 initialSupply) {
_mint(msg.sender, initialSupply);
}
function transfer(address to, uint256 value) external returns (bool) {
_transfer(msg.sender, to, value);
return true;
}
function approve(address spender, uint256 value) external returns (bool) {
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}
function transferFrom(address from, address to, uint256 value) external returns (bool) {
uint256 allowed = allowance[from][msg.sender];
if (allowed != type(uint256).max) {
require(allowed >= value, "allowance");
allowance[from][msg.sender] = allowed - value;
}
_transfer(from, to, value);
return true;
}
function _transfer(address from, address to, uint256 value) internal {
require(to != address(0), "zero address");
require(balanceOf[from] >= value, "balance");
balanceOf[from] -= value;
balanceOf[to] += value;
emit Transfer(from, to, value);
}
function _mint(address to, uint256 value) internal {
totalSupply += value;
balanceOf[to] += value;
emit Transfer(address(0), to, value);
}
}Разбор по частям
Две таблицы
balanceOf[адрес]- сколько токенов у адреса.allowance[владелец][тратящий]- сколько владелец разрешил тратить.
Это всё «состояние» токена. Токены не «лежат» в кошельках - это записи в balanceOf (что хранится в блокчейне). Таблицы хранятся в слотах по ключу.
Знаки после запятой
decimals = 18 значит, что в 1000000000000000000 единиц - один «целый» токен. Контракт работает только с целыми числами; десятичную точку рисует интерфейс (почему). У USDC decimals = 6, у многих токенов 18, и это надо учитывать при расчётах.
transfer
Простой перевод: проверили баланс, уменьшили у отправителя, увеличили у получателя, записали событие. Селектор функции transfer(address,uint256) равен 0xa9059cbb (что такое селектор).
approve и transferFrom
Двухшаговая схема «разрешил - потратили»:
- Владелец вызывает
approve(spender, value). В таблицеallowanceпоявляется запись. - Контракт (spender) вызывает
transferFrom(владелец, получатель, value). Он проверяет разрешение, уменьшает его и выполняет перевод.
Именно так наш эскроу забирает токены покупателя: покупатель сначала одобряет сумму, затем контракт списывает её в createOffer или acceptOffer.
Строка if (allowed != type(uint256).max) - распространённое соглашение: «бесконечное» разрешение не уменьшается, это экономит газ. Но опасно: контракт может потратить все токены (риски).
События
Transfer и Approval - то, по чему кошельки и обозреватели строят историю. Правило: при создании токенов событие Transfer идёт от нулевого адреса (address(0)), при сжигании - к нулевому. Хеш сигнатуры Transfer(address,address,uint256) - первая тема события, начинается с 0xddf252ad (о журнале событий).
Проверка нулевого адреса
require(to != address(0)) защищает от случайной отправки «в никуда», когда токены навсегда теряются (хотя на практике «сожжённые» токены часто отправляют на нулевой или специальный адрес намеренно).
Что здесь упрощено
В минимальной реализации нет:
- Защиты от гонки в approve. Если сменить разрешение с 100 на 50, получатель может успеть потратить 100 до изменения, а потом ещё 50. Решение: сначала обнулять или использовать
increaseAllowance. - Permit (EIP-2612). Разрешение по подписи без отдельной транзакции (подписи).
- Управления выпуском. Кто может создавать токены и как - важнейшая часть дизайна (контроль доступа).
- Пауз и блокировок адресов, которые есть у централизованных стейблкоинов.
Нестандартные токены в реальности
Реальные токены нарушают стандарт по-разному:
- USDT на некоторых сетях не возвращает
boolизtransfer. Контракты должны использоватьSafeERC20, который принимает оба варианта. - Токены с комиссией за перевод (fee-on-transfer) доставляют меньше, чем отправлено. Поэтому эскроу сверяет полученную сумму с заявленной и отклоняет такие токены.
- Ребейзинговые токены сами меняют балансы.
- Блокировки адресов (USDC, USDT): перевод конкретному адресу может отклоняться. В эскроу в этом случае выплата откладывается «кредитом».
Разбор всех подводных камней: особенности ERC-20.
Как проверить свой токен
- Написать тесты: переводы, нулевые суммы, недостаточный баланс,
transferFromбез разрешения (как тестировать). - Фаззинг: сумма всех балансов всегда равна
totalSupply. - Сравнить с реализацией OpenZeppelin.
- Заказать аудит перед запуском на реальные деньги.
Что почитать дальше
Идём к безопасности: каталог уязвимостей. А чтобы запустить собственные тесты, - руководство по Foundry.
Читайте также
- 3 · Под капотом
Нестандартные токены: USDT без return, fee-on-transfer, блэклисты и decimals
Какие сюрпризы скрывают токены ERC-20: USDT без возврата значения, комиссия при переводе, блокировка адресов, разные decimals - и как защищается эскроу.
4 мин чтения - 2 · Практика
ERC-20 и approve: зачем «разрешение» и чем опасен безлимит
Как работают токены ERC-20 (USDT, USDC, DAI), почему перед внесением денег нужна отдельная подпись approve и как безопасно управлять разрешениями.
3 мин чтения
Упоминается в статьях
Готовы попробовать?
Создайте первую сделку за пару минут. Деньги блокируются в смарт-контракте и выплачиваются по правилам, которые видны заранее.

