Перейти к основному содержанию

Пошаговый разбор контракта ERC-20

Solidity
erc-20
Beginner
Ori Pomerantz
9 марта 2021 г.
24 минута прочтения

Введение

Чаще всего 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, или другой контракт, например пул ликвидности.

Иллюстрация интерфейса ERC-20

Если вы опытный программист, вы, вероятно, помните, что видели подобные конструкции в 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-263524729
12 *
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 используется тратящим для фактического использования разрешения.

 

1
2 /**
3 * @dev Создается при перемещении `value` токенов с одного аккаунта (`from`) на
4 * другой (`to`).
5 *
6 * Обратите внимание, что `value` может быть равно нулю.
7 */
8 event Transfer(address indexed from, address indexed to, uint256 value);
9
10 /**
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: MIT
2pragma solidity >=0.6.0 <0.8.0;

 

Инструкции импорта

Помимо определений интерфейса выше, определение контракта импортирует два других файла:

1
2import "../../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.

 

1
2 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' является неявным и может быть опущен.
12
13 _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 }
7
8 /**
9 * @dev Возвращает символ токена, обычно более короткую версию
10 * имени.
11 */
12 function symbol() public view returns (string memory) {
13 return _symbol;
14 }
15
16 /**
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 virtual
15 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)1050
transferFrom(Alice, Bill, 5)10,12305
approve(Bill, 10)11105
transferFrom(Alice, Bill, 10)10,124015

Чтобы избежать этой проблемы, эти две функции (increaseAllowance и decreaseAllowance) позволяют вам изменять разрешение на определенную сумму. Так что, если Билл уже потратил пять токенов, он сможет потратить еще пять. В зависимости от времени это может работать двумя способами, оба из которых заканчиваются тем, что Билл получает только десять токенов:

A:

Транзакция АлисыNonce АлисыТранзакция БиллаNonce БиллаРазрешение для БиллаОбщий доход Билла от Алисы
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
increaseAllowance(Bill, 5)110+5 = 55
transferFrom(Alice, Bill, 5)10,124010

B:

Транзакция АлисыNonce АлисыТранзакция БиллаNonce БиллаРазрешение для БиллаОбщий доход Билла от Алисы
approve(Bill, 5)1050
increaseAllowance(Bill, 5)115+5 = 100
transferFrom(Alice, Bill, 10)10,124010
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, оно не переносится , как обычное сложение.

1
2 /**
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

Есть два способа использовать этот контракт:

  1. Использовать его как шаблон для своего собственного кода
  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: сжигание с нулевого адреса");
14
15 _beforeTokenTransfer(account, address(0), amount);
16
17 _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: разрешение на нулевой адрес");
17
18 _allowances[owner][spender] = amount;
Показать все

 

Инициируйте событие Approval. В зависимости от того, как написано приложение, контракт тратящего может быть уведомлен о разрешении либо владельцем, либо сервером, который прослушивает эти события.

1 emit Approval(owner, spender, amount);
2 }
3

Изменение переменной decimals

1
2
3 /**
4 * @dev Устанавливает {decimals} на значение, отличное от значения по умолчанию 18.
5 *
6 * ВНИМАНИЕ: Эту функцию следует вызывать только из конструктора. Большинство
7 * приложений, взаимодействующих с контрактами токенов, не ожидают,
8 * что {decimals} когда-либо изменится, и могут работать неправильно, если это произойдет.
9 */
10 function _setupDecimals(uint8 decimals_) internal {
11 _decimals = decimals_;
12 }
Показать все

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

Перехватчики

1
2 /**
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 г.

Было ли это руководство полезным?