Пошаговый разбор контракта ERC-20
Введение
Чаще всего Ethereum используется для создания собственного токена, который можно использовать как валюту. Эти токены обычно следуют стандарту, ERC-20. Этот стандарт позволяет создавать такие инструменты, как пулы ликвидности и кошельки, которые работают со всеми токенами ERC-20. В этой статье мы проанализируем реализацию ERC20 на Solidity от OpenZeppelin (opens in a new tab), а также определение интерфейса (opens in a new tab).
Это аннотированный исходный код. Если вы хотите реализовать ERC-20, прочтите это руководство (opens in a new tab).
Интерфейс
Цель такого стандарта, как ERC-20, — обеспечить совместимость множества реализаций токенов в различных приложениях, таких как кошельки и децентрализованные биржи. Для этого мы создаем интерфейс (opens in a new tab). Любой код, которому требуется использовать контракт токена, может использовать те же определения в интерфейсе и быть совместимым со всеми контрактами токенов, которые его используют, будь то кошелек, такой как MetaMask, децентрализованное приложение, такое как etherscan.io, или другой контракт, например пул ликвидности.
Если вы опытный программист, вы, вероятно, помните, что видели подобные конструкции в Java (opens in a new tab) или даже в заголовочных файлах C (opens in a new tab).
Это определение интерфейса ERC-20 (opens in a new tab) от OpenZeppelin. Это перевод человекочитаемого стандарта (opens in a new tab) в код Solidity. Конечно, сам интерфейс не определяет, как что-либо делать. Это объясняется в исходном коде контракта ниже.
1// SPDX-License-Identifier: MITФайлы Solidity должны содержать идентификатор лицензии. Вы можете посмотреть список лицензий здесь (opens in a new tab). Если вам нужна другая лицензия, просто укажите это в комментариях.
1pragma solidity >=0.6.0 <0.8.0;Язык Solidity все еще быстро развивается, и новые версии могут быть несовместимы со старым кодом (см. здесь (opens in a new tab)). Поэтому рекомендуется указывать не только минимальную версию языка, но и максимальную — последнюю, с которой вы тестировали код.
1/**2 * @dev Интерфейс стандарта ERC20, как определено в EIP.3 */@dev в комментарии является частью формата NatSpec (opens in a new tab), используемого для создания
документации из исходного кода.
1interface IERC20 {По соглашению, имена интерфейсов начинаются с I.
1 /**2 * @dev Возвращает количество существующих токенов.3 */4 function totalSupply() external view returns (uint256);Эта функция является external, что означает, что она может быть вызвана только извне контракта (opens in a new tab).
Она возвращает общее количество токенов в контракте. Это значение возвращается с использованием самого распространенного типа в Ethereum, 256-битного беззнакового целого числа (256 бит — это
собственный размер слова EVM). Эта функция также является view, что означает, что она не изменяет состояние, поэтому ее можно выполнить на одном узле, вместо того чтобы ее
выполнял каждый узел в блокчейне. Такая функция не создает транзакцию и не требует Газа.
Примечание. Теоретически может показаться, что создатель контракта может сжульничать, возвращая общее предложение меньше реального, чтобы каждый токен казался более ценным, чем он есть на самом деле. Однако этот страх игнорирует истинную природу блокчейна. Все, что происходит в блокчейне, может быть проверено каждым узлом. Для этого машинный код и хранилище каждого контракта доступны на каждом узле. Хотя вы не обязаны публиковать код Solidity для вашего контракта, никто не воспримет вас всерьез, если вы не опубликуете исходный код и версию Solidity, с которой он был скомпилирован, чтобы его можно было проверить на соответствие предоставленному вами машинному коду. Например, см. этот контракт (opens in a new tab).
1 /**2 * @dev Возвращает количество токенов, принадлежащих `account`.3 */4 function balanceOf(address account) external view returns (uint256);Как следует из названия, balanceOf возвращает баланс аккаунта. Аккаунты Ethereum идентифицируются в Solidity с помощью типа address, который содержит 160 бит.
Она также external и view.
1 /**2 * @dev Перемещает `amount` токенов со счета вызывающего на `recipient`.3 *4 * Возвращает логическое значение, указывающее, успешно ли выполнена операция.5 *6 * Инициирует событие {Transfer}.7 */8 function transfer(address recipient, uint256 amount) external returns (bool);Функция transfer переводит токены с вызывающего адреса на другой адрес. Это связано с изменением состояния, поэтому она не является view.
Когда пользователь вызывает эту функцию, создается транзакция и расходуется Газ. Она также инициирует событие Transfer, чтобы проинформировать всех в
блокчейне об этом событии.
Функция имеет два типа вывода для двух разных типов вызывающих:
- Пользователи, которые вызывают функцию напрямую из пользовательского интерфейса. Обычно пользователь отправляет транзакцию
и не ждет ответа, что может занять неопределенное количество времени. Пользователь может увидеть, что произошло,
поискав квитанцию о транзакции (которая идентифицируется по Хэшу транзакции) или поискав событие
Transfer. - Другие контракты, которые вызывают функцию как часть общей транзакции. Эти контракты получают результат немедленно, потому что они выполняются в той же транзакции, поэтому они могут использовать возвращаемое функцией значение.
Тот же тип вывода создается и другими функциями, которые изменяют состояние контракта.
Разрешения позволяют аккаунту тратить некоторые токены, принадлежащие другому владельцу. Это полезно, например, для контрактов, которые действуют как продавцы. Контракты не могут отслеживать события, поэтому, если покупатель переведет токены на контракт продавца напрямую, этот контракт не узнает, что ему заплатили. Вместо этого покупатель разрешает контракту продавца потратить определенную сумму, и продавец переводит эту сумму. Это делается с помощью функции, которую вызывает контракт продавца, поэтому контракт продавца может узнать, была ли операция успешной.
1 /**2 * @dev Возвращает оставшееся количество токенов, которые `spender` сможет3 * потратить от имени `owner` через {transferFrom}. По умолчанию4 * это ноль.5 *6 * Это значение изменяется при вызове {approve} или {transferFrom}.7 */8 function allowance(address owner, address spender) external view returns (uint256);Функция allowance позволяет любому запросить, какое разрешение один
адрес (owner) дает другому адресу (spender) на трату.
1 /**2 * @dev Устанавливает `amount` в качестве разрешенной суммы для `spender` сверх токенов вызывающей стороны.3 *4 * Возвращает логическое значение, указывающее, успешно ли выполнена операция.5 *6 * ВАЖНО: имейте в виду, что изменение разрешения с помощью этого метода несет в себе риск,7 * что кто-то может использовать и старое, и новое разрешение из-за неудачного8 * порядка транзакций. Одним из возможных решений для смягчения этого состояния гонки9 * является сначала уменьшение разрешения для тратящего до 0, а затем установка10 * желаемого значения:11 * https://github.com/ethereum/EIPs/issues/20#issuecomment-26352472912 *13 * Инициирует событие {Approval}.14 */15 function approve(address spender, uint256 amount) external returns (bool);Показать всеФункция approve создает разрешение. Обязательно прочтите сообщение о том,
как это можно использовать не по назначению. В Ethereum вы контролируете порядок своих собственных транзакций,
но не можете контролировать порядок, в котором будут выполняться транзакции
других людей, если только вы не отправите свою собственную транзакцию после
того, как увидите, что транзакция другой стороны произошла.
1 /**2 * @dev Перемещает токены в размере `amount` от `sender` к `recipient` с использованием3 * механизма разрешений. Затем `amount` вычитается из разрешения4 * вызывающей стороны.5 *6 * Возвращает логическое значение, указывающее, успешно ли выполнена операция.7 *8 * Инициирует событие {Transfer}.9 */10 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);Показать всеНаконец, transferFrom используется тратящим для фактического использования разрешения.
12 /**3 * @dev Создается при перемещении `value` токенов с одного аккаунта (`from`) на4 * другой (`to`).5 *6 * Обратите внимание, что `value` может быть равно нулю.7 */8 event Transfer(address indexed from, address indexed to, uint256 value);910 /**11 * @dev Создается, когда лимит для `spender` от `owner` устанавливается12 * вызовом {approve}. `value` — это новый лимит.13 */14 event Approval(address indexed owner, address indexed spender, uint256 value);15}Показать всеЭти события инициируются при изменении состояния контракта ERC-20.
Сам контракт
Это фактический контракт, реализующий стандарт ERC-20, взятый отсюда (opens in a new tab). Он не предназначен для использования «как есть», но вы можете наследоваться (opens in a new tab) от него, чтобы расширить его до чего-то пригодного для использования.
1// SPDX-License-Identifier: MIT2pragma solidity >=0.6.0 <0.8.0;
Инструкции импорта
Помимо определений интерфейса выше, определение контракта импортирует два других файла:
12import "../../GSN/Context.sol";3import "./IERC20.sol";4import "../../math/SafeMath.sol";GSN/Context.sol— это определения, необходимые для использования OpenGSN (opens in a new tab), системы, которая позволяет пользователям без ether использовать блокчейн. Обратите внимание, что это старая версия, если вы хотите интегрироваться с OpenGSN, используйте это руководство (opens in a new tab).- Библиотека SafeMath (opens in a new tab), которая предотвращает арифметические переполнения/недополнения для версий Solidity <0.8.0. В Solidity ≥0.8.0 арифметические операции автоматически отменяются при переполнении/недополнении, что делает SafeMath ненужной. Этот контракт использует SafeMath для обратной совместимости со старыми версиями компилятора.
Этот комментарий объясняет назначение контракта.
1/**2 * @dev Реализация интерфейса {IERC20}.3 *4 * Эта реализация не зависит от способа создания токенов. Это означает,5 * что механизм предоставления должен быть добавлен в производный контракт с использованием {_mint}.6 * Общий механизм см. в {ERC20PresetMinterPauser}.7 *8 * СОВЕТ: для подробного описания см. наше руководство9 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[Как10 * реализовать механизмы предоставления].11 *12 * Мы следовали общим рекомендациям OpenZeppelin: функции отменяются вместо13 * возврата `false` при сбое. Такое поведение, тем не менее, является общепринятым14 * и не противоречит ожиданиям приложений ERC20.15 *16 * Кроме того, при вызовах {transferFrom} инициируется событие {Approval}.17 * Это позволяет приложениям восстанавливать разрешение для всех аккаунтов, просто18 * прослушивая указанные события. Другие реализации EIP могут не инициировать19 * эти события, поскольку это не требуется спецификацией.20 *21 * Наконец, были добавлены нестандартные функции {decreaseAllowance} и {increaseAllowance}22 * для смягчения известных проблем, связанных с установкой23 * разрешений. См. {IERC20-approve}.24 */25Показать всеОпределение контракта
1contract ERC20 is Context, IERC20 {Эта строка определяет наследование, в данном случае от IERC20 выше и Context для OpenGSN.
12 using SafeMath for uint256;3Эта строка присоединяет библиотеку SafeMath к типу uint256. Вы можете найти эту библиотеку
здесь (opens in a new tab).
Определения переменных
Эти определения определяют переменные состояния контракта. Эти переменные объявлены private, но
это означает только то, что другие контракты в блокчейне не могут их читать. В блокчейне нет
секретов, программное обеспечение на каждом узле имеет состояние каждого контракта
в каждом блоке. По соглашению переменные состояния именуются _<что-то>.
Первые две переменные — это сопоставления (mappings) (opens in a new tab), что означает, что они ведут себя примерно так же, как ассоциативные массивы (opens in a new tab), за исключением того, что ключи являются числовыми значениями. Хранилище выделяется только для записей, которые имеют значения, отличные от значения по умолчанию (ноль).
1 mapping (address => uint256) private _balances;Первое сопоставление, _balances, — это адреса и их соответствующие балансы этого токена. Чтобы получить доступ к
балансу, используйте этот синтаксис: _balances[<address>].
1 mapping (address => mapping (address => uint256)) private _allowances;Эта переменная, _allowances, хранит разрешения, объясненные ранее. Первый индекс — это владелец
токенов, а второй — контракт с разрешением. Чтобы получить доступ к сумме, которую адрес A может
потратить со счета адреса B, используйте _allowances[B][A].
1 uint256 private _totalSupply;Как следует из названия, эта переменная отслеживает общее предложение токенов.
1 string private _name;2 string private _symbol;3 uint8 private _decimals;Эти три переменные используются для улучшения читаемости. Первые две говорят сами за себя, а вот _decimals
— нет.
С одной стороны, в Ethereum нет переменных с плавающей запятой или дробных переменных. С другой стороны, людям нравится иметь возможность делить токены. Одна из причин, по которой люди остановились на золоте в качестве валюты, заключалась в том, что было трудно разменивать деньги, когда кто-то хотел купить утку за часть коровы.
Решение состоит в том, чтобы отслеживать целые числа, но считать не реальный токен, а дробный токен, который почти ничего не стоит. В случае с ether дробный токен называется wei, и 10^18 wei равны одному ETH. На момент написания 10 000 000 000 000 wei примерно равны одному центу США или евро.
Приложениям необходимо знать, как отображать баланс токена. Если у пользователя 3 141 000 000 000 000 000 wei, это
3,14 ETH? 31,41 ETH? 3141 ETH? В случае с ether определено 10^18 wei на ETH, но для вашего
токена вы можете выбрать другое значение. Если деление токена не имеет смысла, вы можете использовать
значение _decimals, равное нулю. Если вы хотите использовать тот же стандарт, что и для ETH, используйте значение 18.
Конструктор
1 /**2 * @dev Устанавливает значения для {name} и {symbol}, инициализирует {decimals} со3 * значением по умолчанию 18.4 *5 * Чтобы выбрать другое значение для {decimals}, используйте {_setupDecimals}.6 *7 * Все три этих значения неизменяемы: их можно установить только один раз во время8 * создания.9 */10 constructor (string memory name_, string memory symbol_) public {11 // В Solidity ≥0.7.0 'public' является неявным и может быть опущен.1213 _name = name_;14 _symbol = symbol_;15 _decimals = 18;16 }Показать всеКонструктор вызывается при первом создании контракта. По соглашению параметры функции именуются <что-то>_.
Функции пользовательского интерфейса
1 /**2 * @dev Возвращает имя токена.3 */4 function name() public view returns (string memory) {5 return _name;6 }78 /**9 * @dev Возвращает символ токена, обычно более короткую версию10 * имени.11 */12 function symbol() public view returns (string memory) {13 return _symbol;14 }1516 /**17 * @dev Возвращает количество десятичных знаков, используемых для получения его представления для пользователя.18 * Например, если `decimals` равно `2`, баланс `505` токенов должен19 * отображаться пользователю как `5,05` (`505 / 10 ** 2`).20 *21 * Токены обычно выбирают значение 18, имитируя отношение между22 * ether и wei. Это значение, которое использует {ERC20}, если не вызывается {_setupDecimals}.23 *24 * ПРИМЕЧАНИЕ: Эта информация используется только для _отображения_: она ни в коем25 * случае не влияет на арифметику контракта, включая26 * {IERC20-balanceOf} и {IERC20-transfer}.27 */28 function decimals() public view returns (uint8) {29 return _decimals;30 }Показать всеЭти функции, name, symbol и decimals, помогают пользовательским интерфейсам узнать о вашем контракте, чтобы они могли правильно его отображать.
Возвращаемый тип — string memory, что означает возврат строки, которая хранится в памяти. Переменные, такие как
строки, могут храниться в трех местах:
| Время жизни | Доступ к контракту | Стоимость Газа | |
|---|---|---|---|
| Память | Вызов функции | Чтение/запись | Десятки или сотни (больше для более высоких местоположений) |
| Calldata | Вызов функции | Только для чтения | Не может использоваться как тип возвращаемого значения, только как тип параметра функции |
| Хранилище | До изменения | Чтение/запись | Высокая (800 для чтения, 20 тыс. для записи) |
В данном случае memory — лучший выбор.
Чтение информации о токене
Это функции, которые предоставляют информацию о токене: общее предложение или баланс аккаунта.
1 /**2 * @dev См. {IERC20-totalSupply}.3 */4 function totalSupply() public view override returns (uint256) {5 return _totalSupply;6 }Функция totalSupply возвращает общее предложение токенов.
1 /**2 * @dev См. {IERC20-balanceOf}.3 */4 function balanceOf(address account) public view override returns (uint256) {5 return _balances[account];6 }Чтение баланса аккаунта. Обратите внимание, что любой может получить баланс аккаунта любого другого. Нет смысла пытаться скрыть эту информацию, потому что она и так доступна на каждом узле. В блокчейне нет секретов.
Перевод токенов
1 /**2 * @dev См. {IERC20-transfer}.3 *4 * Требования:5 *6 * - `recipient` не может быть нулевым адресом.7 * - у вызывающего должен быть баланс не менее `amount`.8 */9 function transfer(address recipient, uint256 amount) public virtual override returns (bool) {Показать всеФункция transfer вызывается для перевода токенов со счета отправителя на другой. Обратите
внимание, что хотя она и возвращает логическое значение, это значение всегда true. Если перевод
не удается, контракт отменяет вызов.
1 _transfer(_msgSender(), recipient, amount);2 return true;3 }Функция _transfer выполняет фактическую работу. Это приватная функция, которая может быть вызвана только
другими функциями контракта. По соглашению приватные функции именуются _<что-то>, так же как и переменные
состояния.
Обычно в Solidity мы используем msg.sender для отправителя сообщения. Однако это нарушает работу
OpenGSN (opens in a new tab). Если мы хотим разрешить транзакции без ether с нашим токеном, нам
нужно использовать _msgSender(). Она возвращает msg.sender для обычных транзакций, но для транзакций без ether
возвращает исходного подписанта, а не контракт, который переслал сообщение.
Функции разрешений
Это функции, которые реализуют функциональность разрешений: allowance, approve, transferFrom
и _approve. Кроме того, реализация OpenZeppelin выходит за рамки базового стандарта и включает некоторые функции, которые улучшают
безопасность: increaseAllowance и decreaseAllowance.
Функция allowance
1 /**2 * @dev См. {IERC20-allowance}.3 */4 function allowance(address owner, address spender) public view virtual override returns (uint256) {5 return _allowances[owner][spender];6 }Функция allowance позволяет всем проверять любое разрешение.
Функция approve
1 /**2 * @dev См. {IERC20-approve}.3 *4 * Требования:5 *6 * - `spender` не может быть нулевым адресом.7 */8 function approve(address spender, uint256 amount) public virtual override returns (bool) {Эта функция вызывается для создания разрешения. Она похожа на функцию transfer выше:
- Функция просто вызывает внутреннюю функцию (в данном случае
_approve), которая выполняет реальную работу. - Функция либо возвращает
true(в случае успеха), либо отменяет операцию (в противном случае).
1 _approve(_msgSender(), spender, amount);2 return true;3 }Мы используем внутренние функции, чтобы минимизировать количество мест, где происходят изменения состояния. Любая функция, изменяющая состояние, является потенциальным риском безопасности, который необходимо проверить на безопасность. Таким образом, у нас меньше шансов ошибиться.
Функция transferFrom
Это функция, которую тратящий вызывает для использования разрешения. Это требует двух операций: перевода потраченной суммы и уменьшения разрешения на эту сумму.
1 /**2 * @dev См. {IERC20-transferFrom}.3 *4 * Инициирует событие {Approval}, указывающее на обновленное разрешение. Это не5 * требуется EIP. См. примечание в начале {ERC20}.6 *7 * Требования:8 *9 * - `sender` и `recipient` не могут быть нулевыми адресами.10 * - у `sender` должен быть баланс не менее `amount`.11 * - у вызывающего должно быть разрешение на токены ``sender`` не менее12 * `amount`.13 */14 function transferFrom(address sender, address recipient, uint256 amount) public virtual15 override returns (bool) {16 _transfer(sender, recipient, amount);Показать все
Вызов функции a.sub(b, "message") выполняет две вещи. Во-первых, он вычисляет a-b, что является новым разрешением.
Во-вторых, он проверяет, что этот результат не является отрицательным. Если он отрицательный, вызов отменяется с предоставленным сообщением. Обратите внимание, что когда вызов отменяется, любая обработка, выполненная ранее во время этого вызова, игнорируется, поэтому нам не нужно
отменять _transfer.
1 _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount,2 "ERC20: сумма перевода превышает разрешение"));3 return true;4 }Дополнения безопасности OpenZeppelin
Опасно устанавливать ненулевое разрешение на другое ненулевое значение, потому что вы контролируете только порядок своих собственных транзакций, а не чьих-либо еще. Представьте, что у вас есть два пользователя: Алиса, которая наивна, и Билл, который нечестен. Алиса хочет получить какую-то услугу от Билла, которая, по ее мнению, стоит пять токенов, поэтому она дает Биллу разрешение на пять токенов.
Затем что-то меняется, и цена Билла повышается до десяти токенов. Алиса, которая все еще хочет получить услугу, отправляет транзакцию, которая устанавливает разрешение Билла на десять. В тот момент, когда Билл видит эту новую транзакцию в пуле транзакций, он отправляет транзакцию, которая тратит пять токенов Алисы и имеет гораздо более высокую цену на Газ, чтобы она была добыта быстрее. Таким образом, Билл может сначала потратить пять токенов, а затем, когда новое разрешение Алисы будет добыто, потратить еще десять, что в общей сложности составит пятнадцать токенов — больше, чем Алиса намеревалась разрешить. Этот метод называется опережением (фронтраннинг) (opens in a new tab)
| Транзакция Алисы | Nonce Алисы | Транзакция Билла | Nonce Билла | Разрешение для Билла | Общий доход Билла от Алисы |
|---|---|---|---|---|---|
| approve(Bill, 5) | 10 | 5 | 0 | ||
| transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | ||
| approve(Bill, 10) | 11 | 10 | 5 | ||
| transferFrom(Alice, Bill, 10) | 10,124 | 0 | 15 |
Чтобы избежать этой проблемы, эти две функции (increaseAllowance и decreaseAllowance) позволяют вам
изменять разрешение на определенную сумму. Так что, если Билл уже потратил пять токенов, он сможет
потратить еще пять. В зависимости от времени это может работать двумя способами, оба из
которых заканчиваются тем, что Билл получает только десять токенов:
A:
| Транзакция Алисы | Nonce Алисы | Транзакция Билла | Nonce Билла | Разрешение для Билла | Общий доход Билла от Алисы |
|---|---|---|---|---|---|
| approve(Bill, 5) | 10 | 5 | 0 | ||
| transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | ||
| increaseAllowance(Bill, 5) | 11 | 0+5 = 5 | 5 | ||
| transferFrom(Alice, Bill, 5) | 10,124 | 0 | 10 |
B:
| Транзакция Алисы | Nonce Алисы | Транзакция Билла | Nonce Билла | Разрешение для Билла | Общий доход Билла от Алисы |
|---|---|---|---|---|---|
| approve(Bill, 5) | 10 | 5 | 0 | ||
| increaseAllowance(Bill, 5) | 11 | 5+5 = 10 | 0 | ||
| transferFrom(Alice, Bill, 10) | 10,124 | 0 | 10 |
1 /**2 * @dev Атомарно увеличивает разрешение, предоставленное `spender` вызывающей стороной.3 *4 * Это альтернатива {approve}, которая может использоваться для смягчения5 * проблем, описанных в {IERC20-approve}.6 *7 * Инициирует событие {Approval}, указывающее на обновленное разрешение.8 *9 * Требования:10 *11 * - `spender` не может быть нулевым адресом.12 */13 function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {14 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue));15 return true;16 }Показать всеФункция a.add(b) — это безопасное сложение. В маловероятном случае, если a+b>=2^256, оно не переносится
, как обычное сложение.
12 /**3 * @dev Атомарно уменьшает разрешение, предоставленное `spender` вызывающей стороной.4 *5 * Это альтернатива {approve}, которая может использоваться для смягчения6 * проблем, описанных в {IERC20-approve}.7 *8 * Инициирует событие {Approval}, указывающее на обновленное разрешение.9 *10 * Требования:11 *12 * - `spender` не может быть нулевым адресом.13 * - `spender` должен иметь разрешение для вызывающей стороны не менее14 * `subtractedValue`.15 */16 function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {17 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue,18 "ERC20: уменьшенное разрешение ниже нуля"));19 return true;20 }Показать всеФункции, изменяющие информацию о токене
Это четыре функции, которые выполняют фактическую работу: _transfer, _mint, _burn и _approve.
Функция _transfer
1 /**2 * @dev Перемещает токены в размере `amount` от `sender` к `recipient`.3 *4 * Эта внутренняя функция эквивалентна {transfer} и может использоваться для5 * реализации, например, автоматических комиссий за токены, механизмов слэшинга и т. д.6 *7 * Инициирует событие {Transfer}.8 *9 * Требования:10 *11 * - `sender` не может быть нулевым адресом.12 * - `recipient` не может быть нулевым адресом.13 * - у `sender` должен быть баланс не менее `amount`.14 */15 function _transfer(address sender, address recipient, uint256 amount) internal virtual {Показать всеЭта функция, _transfer, переводит токены с одного аккаунта на другой. Она вызывается как
transfer (для переводов с собственного счета отправителя), так и transferFrom (для использования разрешений
для перевода со счета другого лица).
1 require(sender != address(0), "ERC20: перевод с нулевого адреса");2 require(recipient != address(0), "ERC20: перевод на нулевой адрес");Никто на самом деле не владеет нулевым адресом в Ethereum (то есть никто не знает приватный ключ, соответствующий публичный ключ которого преобразуется в нулевой адрес). Когда люди используют этот адрес, это обычно ошибка программного обеспечения, поэтому мы прерываем операцию, если нулевой адрес используется как отправитель или получатель.
1 _beforeTokenTransfer(sender, recipient, amount);2Есть два способа использовать этот контракт:
- Использовать его как шаблон для своего собственного кода
- Наследоваться от него (opens in a new tab) и переопределять только те функции, которые вам нужно изменить
Второй метод намного лучше, потому что код OpenZeppelin ERC-20 уже был проверен и признан безопасным. Когда вы используете наследование, ясно, какие функции вы изменяете, и чтобы доверять вашему контракту, людям нужно проверить только эти конкретные функции.
Часто бывает полезно выполнять функцию каждый раз, когда токены переходят из рук в руки. Однако _transfer — очень важная функция, и ее можно
написать небезопасно (см. ниже), поэтому лучше ее не переопределять. Решение — _beforeTokenTransfer,
функция-перехватчик (hook) (opens in a new tab). Вы можете переопределить эту функцию, и она будет вызываться при каждом переводе.
1 _balances[sender] = _balances[sender].sub(amount, "ERC20: сумма перевода превышает баланс");2 _balances[recipient] = _balances[recipient].add(amount);Это строки, которые фактически выполняют перевод. Обратите внимание, что между ними ничего нет, и что мы вычитаем переведенную сумму у отправителя перед тем, как добавить ее получателю. Это важно, потому что если бы посредине был вызов другого контракта, его можно было бы использовать для обмана этого контракта. Таким образом, перевод является атомарным, ничего не может произойти в его середине.
1 emit Transfer(sender, recipient, amount);2 }Наконец, инициируйте событие Transfer. События недоступны для Умных контрактов, но код, работающий вне блокчейна,
может прослушивать события и реагировать на них. Например, кошелек может отслеживать, когда владелец получает больше токенов.
Функции _mint и _burn
Эти две функции (_mint и _burn) изменяют общее предложение токенов.
Они являются внутренними, и в этом контракте нет функции, которая их вызывает,
поэтому они полезны только в том случае, если вы наследуете от контракта и добавляете свою собственную
логику для решения, при каких условиях создавать новые токены или сжигать существующие.
ПРИМЕЧАНИЕ: У каждого токена ERC-20 своя бизнес-логика, которая диктует управление токенами.
Например, контракт с фиксированным предложением может вызывать _mint только
в конструкторе и никогда не вызывать _burn. Контракт, который продает токены,
будет вызывать _mint при получении оплаты и, предположительно, вызывать _burn в какой-то момент,
чтобы избежать безудержной инфляции.
1 /** @dev Создает `amount` токенов и назначает их `account`, увеличивая2 * общее предложение.3 *4 * Инициирует событие {Transfer} с `from`, установленным на нулевой адрес.5 *6 * Требования:7 *8 * - `to` не может быть нулевым адресом.9 */10 function _mint(address account, uint256 amount) internal virtual {11 require(account != address(0), "ERC20: создание токенов на нулевой адрес");12 _beforeTokenTransfer(address(0), account, amount);13 _totalSupply = _totalSupply.add(amount);14 _balances[account] = _balances[account].add(amount);15 emit Transfer(address(0), account, amount);16 }Показать всеНе забудьте обновить _totalSupply при изменении общего количества токенов.
1 /**2 * @dev Уничтожает `amount` токенов с `account`, уменьшая3 * общее предложение.4 *5 * Инициирует событие {Transfer} с `to`, установленным на нулевой адрес.6 *7 * Требования:8 *9 * - `account` не может быть нулевым адресом.10 * - `account` должен иметь не менее `amount` токенов.11 */12 function _burn(address account, uint256 amount) internal virtual {13 require(account != address(0), "ERC20: сжигание с нулевого адреса");1415 _beforeTokenTransfer(account, address(0), amount);1617 _balances[account] = _balances[account].sub(amount, "ERC20: сумма сжигания превышает баланс");18 _totalSupply = _totalSupply.sub(amount);19 emit Transfer(account, address(0), amount);20 }Показать всеФункция _burn почти идентична _mint, за исключением того, что она работает в обратном направлении.
Функция _approve
Это функция, которая фактически определяет разрешения. Обратите внимание, что она позволяет владельцу указать разрешение, превышающее текущий баланс владельца. Это нормально, потому что баланс проверяется во время перевода, когда он может отличаться от баланса при создании разрешения.
1 /**2 * @dev Устанавливает `amount` в качестве разрешения `spender` над токенами `owner`.3 *4 * Эта внутренняя функция эквивалентна `approve` и может использоваться для5 * установки, например, автоматических разрешений для определенных подсистем и т. д.6 *7 * Инициирует событие {Approval}.8 *9 * Требования:10 *11 * - `owner` не может быть нулевым адресом.12 * - `spender` не может быть нулевым адресом.13 */14 function _approve(address owner, address spender, uint256 amount) internal virtual {15 require(owner != address(0), "ERC20: разрешение с нулевого адреса");16 require(spender != address(0), "ERC20: разрешение на нулевой адрес");1718 _allowances[owner][spender] = amount;Показать все
Инициируйте событие Approval. В зависимости от того, как написано приложение, контракт тратящего может быть уведомлен о
разрешении либо владельцем, либо сервером, который прослушивает эти события.
1 emit Approval(owner, spender, amount);2 }3Изменение переменной decimals
123 /**4 * @dev Устанавливает {decimals} на значение, отличное от значения по умолчанию 18.5 *6 * ВНИМАНИЕ: Эту функцию следует вызывать только из конструктора. Большинство7 * приложений, взаимодействующих с контрактами токенов, не ожидают,8 * что {decimals} когда-либо изменится, и могут работать неправильно, если это произойдет.9 */10 function _setupDecimals(uint8 decimals_) internal {11 _decimals = decimals_;12 }Показать всеЭта функция изменяет переменную _decimals, которая используется для того, чтобы сообщить пользовательским интерфейсам, как интерпретировать сумму.
Вы должны вызывать ее из конструктора. Было бы нечестно вызывать ее в любой последующий момент, и приложения
не предназначены для обработки этого.
Перехватчики
12 /**3 * @dev Перехватчик, который вызывается перед любым переводом токенов. Это включает4 * создание и сжигание.5 *6 * Условия вызова:7 *8 * - когда `from` и `to` оба не равны нулю, `amount` токенов ``from``9 * будет переведено `to`.10 * - когда `from` равно нулю, `amount` токенов будет создано для `to`.11 * - когда `to` равно нулю, `amount` токенов ``from`` будет сожжено.12 * - `from` и `to` никогда не равны нулю одновременно.13 *14 * Чтобы узнать больше о перехватчиках, перейдите к xref:ROOT:extending-contracts.adoc#using-hooks[Использование перехватчиков].15 */16 function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }17}Показать всеЭто функция-перехватчик, которая вызывается во время переводов. Здесь она пуста, но если вам нужно, чтобы она что-то делала, вы просто переопределяете ее.
Заключение
Для обзора, вот некоторые из самых важных идей в этом контракте (по моему мнению, ваше может отличаться):
- В блокчейне не существует секретов. Любая информация, к которой может получить доступ Умный контракт, доступна всему миру.
- Вы можете контролировать порядок своих собственных транзакций, но не то, когда происходят транзакции других людей. Именно по этой причине изменение разрешения может быть опасным, потому что оно позволяет тратящему потратить сумму обоих разрешений.
- Значения типа
uint256циклически переносятся. Другими словами, 0-1=2^256-1. Если это нежелательное поведение, вы должны проверять его (или использовать библиотеку SafeMath, которая делает это за вас). Обратите внимание, что это изменилось в Solidity 0.8.0 (opens in a new tab). - Выполняйте все изменения состояния определенного типа в определенном месте, потому что это облегчает аудит.
Именно по этой причине у нас есть, например,
_approve, который вызываетсяapprove,transferFrom,increaseAllowanceиdecreaseAllowance - Изменения состояния должны быть атомарными, без каких-либо других действий в их середине (как вы можете видеть
в
_transfer). Это связано с тем, что во время изменения состояния у вас несогласованное состояние. Например, между моментом вычета из баланса отправителя и моментом добавления к балансу получателя существует меньше токенов, чем должно быть. Этим потенциально можно злоупотребить, если между ними есть операции, особенно вызовы другого контракта.
Теперь, когда вы увидели, как написан контракт OpenZeppelin ERC-20, и особенно как он сделан более безопасным, идите и пишите свои собственные безопасные контракты и приложения.
Больше моих работ смотрите здесь (opens in a new tab).
Последнее обновление страницы: 22 октября 2025 г.
