Стандарты токенов: ERC-20 и его родня
Смарт-контракт может делать что угодно, но экосистеме нужен общий язык. Кошелёк должен знать, как показать баланс любого токена, биржа как его обменять, не разбираясь в каждом контракте отдельно. Так родились стандарты токенов: договорённость об именах функций и событий, которую соблюдают все. Разберём ERC-20 и его наследников и посмотрим, где в токенах прячутся уязвимости на реальные деньги.
Идея
ERC-20 это стандарт взаимозаменяемого токена: все единицы одинаковы, как рубли на счёте. Стандарт это просто набор функций и событий, которые контракт обязан реализовать, чтобы любой кошелёк и любая биржа умели с ним работать без спецкода.
Минимальный ERC-20 это одна табличка балансов плюс договорённость об именах:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract MiniToken {
string public name = "Mini";
string public symbol = "MINI";
uint8 public 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 initial) {
totalSupply = initial;
balanceOf[msg.sender] = initial;
emit Transfer(address(0), msg.sender, initial); // чеканка это перевод из нуля
}
function transfer(address to, uint256 amount) external returns (bool) {
balanceOf[msg.sender] -= amount; // 0.8 сам откатит при недостатке средств
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
}
Заметь: токен это не отдельная монета, а строчка в mapping внутри одного контракта. «Перевести токен» значит уменьшить одно число и увеличить другое. Событие Transfer из урока 27 это то, по чему кошельки видят движение средств.
approve и allowance: разрешить потратить за тебя
Как дать бирже право снять твои токены при свопе, не отдавая ей ключи? Через двухшаговый паттерн allowance:
approve(spender, amount)выставляет лимит: «этому адресу можно снять до столько».transferFrom(owner, to, amount)вызывает уже сам spender, пока лимит не исчерпан.
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
allowance[from][msg.sender] -= amount; // тратим из разрешения, откат при превышении
balanceOf[from] -= amount;
balanceOf[to] += amount;
emit Transfer(from, to, amount);
return true;
}
Это фундамент композируемости DeFi: разрешил протоколу тратить твои токены, и он действует от твоего имени в рамках лимита. Весь DEX, который мы соберём дальше в курсе, стоит на этом.
Невзаимозаменяемые и мульти: ERC-721, ERC-1155, ERC-4626
ERC-20 задал шаблон, и от него отпочковалась семья:
- ERC-721 это NFT: каждый токен уникален, у него свой
tokenIdи один владелец. Вместо балансов картаtokenId → owner. Метаданные (картинка, свойства) лежат по ссылке изtokenURI. - ERC-1155 это мульти-токен: один контракт держит сразу много видов токенов и умеет пакетные переводы, экономя газ. Родом из игр, где предметов тысячи.
- ERC-4626 это токенизированное хранилище: вносишь актив, получаешь доли, которые растут по мере дохода протокола. Стандарт доходных стратегий DeFi.
Чтобы слова про ERC-721 не остались словами, вот ядро его интерфейса. Оно выглядит знакомо после ERC-20, но обрати внимание: везде гуляет tokenId, а не количество.
interface IERC721 {
// третий indexed это tokenId: переводится конкретный токен, а не сумма
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
function balanceOf(address owner) external view returns (uint256); // сколько NFT у адреса
function ownerOf(uint256 tokenId) external view returns (address); // кто владеет токеном
function transferFrom(address from, address to, uint256 tokenId) external;
}
Про ERC-4626 стоит рассказать, зачем ему вообще стандартизация, на живой атаке. Хранилище выдаёт доли пропорционально активам, и на пустом хранилище пропорция вырождается. Атакующий первым кладёт 1 wei и получает 1 долю, а потом переводит крупную сумму на адрес хранилища напрямую, обычным transfer, мимо deposit: активов стало много, а долей по-прежнему одна. Теперь честный вкладчик вносит депозит, его доля вычисляется делением с округлением вниз и получается ноль (или почти ноль), и атакующий, владея единственной долей, забирает и свой донат, и чужой депозит. Это и есть inflation-атака. Стандарт с эталонной реализацией решает её системно: виртуальные доли и сдвиг десятичных знаков (virtual shares, decimals offset) делают так, будто в хранилище всегда есть немного активов и долей, и раздуть цену одной доли донатом становится экономически бессмысленно. Когда каждый протокол писал свой vault сам, каждый наступал на эти грабли заново.
премиум-урок
Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.